/**
 * yeganaaa@163.com
 * author: (مۇختەرجان مەخمۇت) muhtaer mahmood
 * 10/13/2019, 1:40:26 PM GMT+06:00
 */

import {v4 as uuid} from "uuid"
import {getMetaDataObject, isNodeRuntime} from "./Helper"
import apply = Reflect.apply;
import { isBuffer } from "util";

/**
 * Rpc call request object
 */
class RpcFunctionRequest<RETURN_TYPE, PARAMETER_TYPE> {
    public id: string = uuid().replace("-", "")

    public constructor(
        /**
         * remote method path, e.g. /aaa/bbb/ccc or for simple /sayHello
         */
        public name: string,
        /**
         * parameter for remote method, this is can be simple value type object or complex object can be serialize json or array buffer.
         */
        public parameter: Uint8Array,
    ) {

    }
}

/**
 * Rpc call response object
 */
class RpcFunctionResponse<RETURN_TYPE, PARAMETER_TYPE> {
    public id: string = ""
    public errorCode: number = RpcFunctionResponse.ERROR_CODES().NO_ERROR
    public errorMessage: string = ""

    public constructor(public request: RpcFunctionRequest<RETURN_TYPE, PARAMETER_TYPE>, public data: Uint8Array) {
        this.id = request.id;
        this.request = undefined!
    }

    public static ERROR_CODES() {
        return {
            NO_ERROR: 0x00,
            METHOD_NOT_FOUND: 0x01,
            RUNTIME_EXCEPTION: 0x02,
        }
    }

    public apply(action: (self: RpcFunctionResponse<RETURN_TYPE, PARAMETER_TYPE>) => void) {
        action(this)
        return this
    }
}

class RpcMetaData {
    public constructor(public name: string, public type: RpcMetaType = RpcMetaType.Function, public modules: Array<RpcMetaData> = []) {
    }
}

enum RpcMetaType {
    Module,
    Function
}

export interface IRpcSocketServer<SERVER> {
    server: SERVER
    prepareForLaunch(): Promise<void>
    shutdown(): Promise<void>
    onLaunch: () => void
    onClientConnected: (client: RpcBridgeHost<any>) => void
    onError: (error: Error) => void
    onShutDown: () => void
}

abstract class RpcBridgeHost<SOCKET> {
    abstract socket: SOCKET
    onConnected: () => void = null!
    onMessageReceived: (message: Uint8Array) => void = null!
    onError: (error: Error) => void = null!
    onClosed: () => void = null!

    // abstract open(callback: () => void): void
    abstract close(callback: () => void): void

    abstract send(data: Uint8Array, callback: (error: Error) => void): void

    abstract initialize(): Promise<void>
}

enum RequestType {
    rpcMetaData,
    rpcRequest,
    rpcResponse,
    nonRpc
}

class Message {
    public constructor(public type: RequestType, public data: RpcFunctionRequest<any, any> | RpcFunctionResponse<any, any> | RpcMetaData, public header: IRpcHeader) {

    }
}

interface IRpcBridgeEvents {
    onReady: () => void// = () => { console.log("Connected") },
    onMessageReceived: (message: any) => void// = message => { },
    onError: (error: Error) => void// = error => { console.error("error")},
    onClosed: () => void //= () => { console.log("Closed")},
}

/**
 * Rpc events, e.g. onConnected, onMessageREceived, onClosed, onError etc.
 */
class RpcBridgeEvents implements IRpcBridgeEvents {
    public constructor(
        /**
         * call when connect successfully remote way.
         */
        public onReady: () => void = () => {
            console.log("Connected")
        },
        /**
         * call when any message received from remote way included RpcRequest receive, Meta Data receive.
         * @param message is received message
         */
        public onMessageReceived: (message: any) => void = message => {
        },
        /**
         *call when any error occurred.
         * @param error is error message
         */
        public onError: (error: Error) => void = error => {
            console.error(error)
        },
        /**
         * call when connection is disconnected.
         */
        public onClosed: () => void = () => {
            console.log("Closed")
        },
    ) {

    }
}

export class PromiseCompleter<T> {
    private _resolve: (parameter: T) => void = null!
    private _reject: (error: any) => void = null!
    public completer: Promise<T> = null!
    resolve(parameter: T) {
        this._CHECK_NULLABLE()
        this._resolve(parameter)
        this._RESET()
    }
    reject(error: any) {
        this._CHECK_NULLABLE()
        this._reject(error)
        this._RESET()
    }
    private _CHECK_NULLABLE(){
        let valid = true
        valid = valid && this._resolve != null
        valid = valid && this._reject != null
        if (!valid) throw new Error("Please call build method to build new Promise object before resolve or reject.")
    }
    private _RESET(){
        this._resolve = null!
        this._reject = null!
    }
    build(): Promise<T> {
        this.completer = new Promise<T>((resolve, reject) => {
            this._resolve = (parameter) => {
                resolve(parameter)
            }
            this._reject = error => {
                reject(error)
            }
        })
        return this.completer
    }
}
export namespace Rpc {
    export function initializeHiddenMetaObjectIfNotExists(obj: any) {
        if (obj.__meta__ == null) obj.__meta__ = {}
        if (obj.__meta__.ignored == null) obj.__meta__.ignored = []
        if (obj.__meta__.noSerialize == null) obj.__meta__.noSerialize = []
        if (obj.__meta__.noDeserialize == null) obj.__meta__.noDeserialize = []
    }
    export function ignoreThisMethod() {
        return (obj: any, method: string, descriptor: PropertyDescriptor) => {
            initializeHiddenMetaObjectIfNotExists(obj)
            obj.__meta__.ignored.push(method)
        }
    }
    export function ignoreThisProperty() {
        return (obj: any, property: string) => {
            initializeHiddenMetaObjectIfNotExists(obj)
            obj.__meta__.ignored.push(property)
        }
    }
    export function hasInIgnoreList(obj: any, memberName: string) {
        initializeHiddenMetaObjectIfNotExists(obj)
        return obj.__meta__.ignored.filter((item: any) => item == memberName).length > 0
    }
}
class MessageProtocol {
    public static deserializeMessage(buffer: Uint8Array): Message {
        //////PROTOCOL DEFINITION//////
        ///[4 BYTE MESSAGE HEADER LENGTH][...MESSAGE HEADER...][4 BYTE MESSAGE REQUEST LENGTH][...MESSAGE REQUEST...][4 BYTE MESSAGE REQUEST PARAMETER LENGTH][...MESSAGE REQUEST PARAMETER...]
        //////PROTOCOL DEFINITION//////
        const textDecoder = new TextDecoder()
        const dataView = new DataView(buffer.buffer, buffer.byteOffset, buffer.length)
        let offset = 0
        const requestType = dataView.getUint32(offset, true)
        offset += 4
        const headerWidth = dataView.getUint32(offset, true)
        offset += 4
        const text = textDecoder.decode(buffer.slice(offset, offset + headerWidth))
        const headerObject = JSON.parse(text)
        offset += headerWidth
        const requestLength = dataView.getUint32(offset, true)
        offset += 4
        const request = buffer.slice(offset, offset + requestLength)
        offset += requestLength
        const parameterLength = dataView.getUint32(offset, true)
        offset += 4
        const parameter = buffer.slice(offset, offset + parameterLength)
        let requestObject: RpcFunctionRequest<any, any> | RpcFunctionResponse<any, any> | RpcMetaData = null!
        if (requestType != RequestType.nonRpc) requestObject = JSON.parse(textDecoder.decode(request))
        if (requestType == RequestType.rpcMetaData) requestObject = requestObject as RpcMetaData //do not anything
        if (requestType == RequestType.rpcRequest) {
            requestObject = requestObject as RpcFunctionRequest<any, any>
            requestObject.parameter = parameter
        }
        if (requestType == RequestType.rpcResponse) {
            requestObject = requestObject as RpcFunctionResponse<any, any>
            requestObject.data = parameter
        }
        const message = new Message(requestType, requestObject, headerObject)
        return message
    }
    public static serializeMessage(message: Message): Uint8Array {
        //////PROTOCOL DEFINITION//////
        ///[4 BYTE MESSAGE HEADER LENGTH][...MESSAGE HEADER...][4 BYTE MESSAGE REQUEST LENGTH][...MESSAGE REQUEST...][4 BYTE MESSAGE REQUEST PARAMETER LENGTH][...MESSAGE REQUEST PARAMETER...]
        //////PROTOCOL DEFINITION//////

        const textEncoder = new TextEncoder()
        const header = textEncoder.encode(JSON.stringify(message.header))
        let parameter = new ArrayBuffer(0)
        if (message.type == RequestType.rpcRequest) {
            const data = (message.data as RpcFunctionRequest<any, any>)
            parameter = data.parameter
            data.parameter = undefined!
        }
        if (message.type == RequestType.rpcResponse) {
            const data = (message.data as RpcFunctionResponse<any, any>)
            parameter = data.data
            data.data = undefined!
        }
        const request = textEncoder.encode(JSON.stringify(message.data))
        const buffer = new Uint8Array(4 + 4 + header.length + 4 + request.length + 4 + parameter.byteLength)
        const dataView = new DataView(buffer.buffer)
        let offset = 0
        dataView.setUint32(offset, message.type, true)
        offset += 4
        dataView.setUint32(offset, header.length, true)
        offset += 4
        buffer.set(header, offset)
        offset += header.length
        dataView.setUint32(offset, request.length, true)
        offset += 4
        buffer.set(request, offset)
        offset += request.length
        dataView.setUint32(offset, parameter.byteLength, true)
        offset += 4
        buffer.set(new Uint8Array(parameter), offset)
        offset += parameter.byteLength
        return buffer
    }
}
export interface IRpcHeader {
    contentType: string
    [key: string]: any
}
/**
 * Rpc bridge object, this object is a bridge between local program and remote program,
 */
class RpcBridge<LOCAL_INTERFACE, REMOTE_INTERFACE> {
    private localRpcMetaData: RpcMetaData = new RpcMetaData("")
    private remoteRpcMetaData: RpcMetaData = new RpcMetaData("")
    public remoteInterface: REMOTE_INTERFACE = {} as REMOTE_INTERFACE
    private callResponseQueue = new Map<string, (result: RpcFunctionResponse<any, any>, headers: IRpcHeader, error?: Error) => void>()
    public events: IRpcBridgeEvents = new RpcBridgeEvents()
    public onReadyCompleter = new PromiseCompleter<void>()
    private _initializationTimeoutId: NodeJS.Timeout = null!
    private _initializeCompleter = new PromiseCompleter<void>()

    public constructor(public bridgeHost: RpcBridgeHost<any>, public localInterface: LOCAL_INTERFACE) {
        this.onReadyCompleter.build()
        this._initializeCompleter.build()
    }
    public initialize(timeOutMilliseconds: number = 10_000): Promise<void> {
        this._initializationTimeoutId = setTimeout(() => {
            this.onReadyCompleter.reject(new Error(`${timeOutMilliseconds} milliSecond time is Timeout while initialize.`))
        }, timeOutMilliseconds)
        this.initLocalMetaData()
        this.bridgeHost.onMessageReceived = async data => {
            const message = MessageProtocol.deserializeMessage(data)
            if (message.type == RequestType.nonRpc) return
            if (message.type == RequestType.rpcMetaData) {
                this.onRemoteInterfaceMetaDataReceived(message.data as RpcMetaData)
                return
            }
            if (message.type == RequestType.rpcRequest) {
                await this.onReadyCompleter.completer
                const receivedMessage = message.data as RpcFunctionRequest<any, any>
                this.rpcCallRequestReceived(receivedMessage, message.header)
                return
            }
            if (message.type == RequestType.rpcResponse) {
                await this.onReadyCompleter.completer
                const response = message.data as RpcFunctionResponse<any, any>
                const action = this.callResponseQueue.get(response.id)!
                this.events.onMessageReceived(response.data) //OnMessage response event.
                let error = new Error(JSON.stringify({
                    message: response.errorMessage,
                    code: response.errorCode
                }))
                if (response.errorCode == RpcFunctionResponse.ERROR_CODES().NO_ERROR) {
                    error = null!
                }
                action(response, message.header, error)
                return
            }
        }

        this.bridgeHost.onConnected = async () => {
            await this._initializeCompleter.completer
            const message = new Message(RequestType.rpcMetaData, this.localRpcMetaData, {contentType: "json"})
            this.bridgeHost.send(new Uint8Array(MessageProtocol.serializeMessage(message)), error => {
                if (error) console.error(error)
            })
        }

        this.bridgeHost.onError = (error) => {
            this.events.onError(error)
            console.error(error)
        }

        this.bridgeHost.onClosed = () => {
            this.events.onClosed()
        }
        return this.bridgeHost.initialize().then(_ => {
            this._initializeCompleter.resolve()
            return this.onReadyCompleter.completer
        })
        // return new Promise<void>(async (resolve, reject) => {
        //     await this.bridgeHost.initialize()
        //     await this.onReadyCompleter.completer
        //     resolve()
        // })
    }

    /**
     * called this method internally when metadata received from remote way
     * @param metaData
     */
    private onRemoteInterfaceMetaDataReceived(metaData: RpcMetaData) {
        this.remoteRpcMetaData = metaData
        for (let key in this.remoteInterface as any) {
            delete (this.remoteInterface as any)[key]
        }
        this.remoteInterface = this.declareModule(this.remoteRpcMetaData)
        this.events.onReady()
        this.onReadyCompleter.resolve()
        clearTimeout(this._initializationTimeoutId)
    }

    /**
     * declare module into delegate object
     * @param meta Meta data
     */
    private declareModule(meta: RpcMetaData): any {
        const module: any = {}
        function getPlainNameOfMethodOrModule(nameHierarchy: string){
            return nameHierarchy.substring(nameHierarchy.lastIndexOf("/") + 1)
        }
        meta.modules.filter(module => module.type == RpcMetaType.Module).forEach(item => {
            module[getPlainNameOfMethodOrModule(item.name)] = this.declareModule(item)
        })
        meta.modules.filter(module => module.type == RpcMetaType.Function).forEach(item => {
            module[getPlainNameOfMethodOrModule(item.name)] = this.declareMethod(item)
        })
        return module
    }

    /**
     Declare method into delegate object or module
     */
    private declareMethod(meta: RpcMetaData): (args: any, header: IRpcHeader) => Promise<any> {
        const action = (args: any): Promise<any> => {
            const header: IRpcHeader = {contentType: "binary"}
            return new Promise((resolve, reject) => {
                if (args == null) args = {}
                if (!this.isBinaryDataObject(args)) {
                    args = new TextEncoder().encode(JSON.stringify(args))
                    header.contentType = "json"
                }
                const callRequest = new RpcFunctionRequest(meta.name, args)
                const message = new Message(RequestType.rpcRequest, callRequest, header)
                this.callResponseQueue.set(callRequest.id, (result, header, error) => {
                    this.callResponseQueue.delete(callRequest.id)
                    if (error) {
                        this.events.onError(error)
                        reject(error)
                        return
                    }
                    resolve(header.contentType == "binary" ? result.data : JSON.parse(new TextDecoder().decode(result.data)))
                })

                this.bridgeHost.send(new Uint8Array(MessageProtocol.serializeMessage(message)), error => {
                    if (error) {
                        console.error(error)
                        reject(error)
                    }
                })
            })
        }
        return action
    }
    /**
     When rpc call request
     */
    private rpcCallRequestReceived(rpcCallRequest: RpcFunctionRequest<any, any>, header: IRpcHeader) {
        let nameOfAction = rpcCallRequest.name
        const error = new Error(`Rpc method '${nameOfAction}' not found in remote way.`)
        let response = new RpcFunctionResponse(rpcCallRequest, new TextEncoder().encode(JSON.stringify(error))).apply(self => {
            self.errorCode = RpcFunctionResponse.ERROR_CODES().METHOD_NOT_FOUND
            self.errorMessage = error.message
        })
        let responseHeaders: IRpcHeader = {contentType: "binary"}
        //if (this.localRpcMetaData.meta.filter(meta => meta == nameOfAction).length != 0)
        const proxyObjectHierarchy = nameOfAction.split("/").filter(it => it != "")
        let action: any = this.localInterface
        let ownerOfAction: any = this.localInterface
        for (let index = 0; index < proxyObjectHierarchy.length; index++) {
            action = action[proxyObjectHierarchy[index]]
            if (proxyObjectHierarchy.length - 1 - 1 == index) ownerOfAction = action
        }

        const send = () => {
            const message = new Message(RequestType.rpcResponse, response, responseHeaders)
            this.bridgeHost.send(new Uint8Array(MessageProtocol.serializeMessage(message)), (error) => {
                if (error) {
                    console.error(error)
                    this.events.onError(error)
                }
            })
        }
        if (action != null) {
            const result = (action as (param: any, header?: any) => Promise<any>).call(ownerOfAction, header.contentType == "json" ? JSON.parse(new TextDecoder().decode(rpcCallRequest.parameter)) : rpcCallRequest.parameter) as Promise<any>
            result.then(value => {
                response.errorCode = RpcFunctionResponse.ERROR_CODES().NO_ERROR
                response.errorMessage = ""
                if (value == null) value = {}

                if (!this.isBinaryDataObject(value)) {
                    value = new TextEncoder().encode(JSON.stringify(value))
                    responseHeaders.contentType = "json"
                }
                response.data = value
            }).catch(error => {
                this.events.onError(error)
                response.errorCode = RpcFunctionResponse.ERROR_CODES().RUNTIME_EXCEPTION
                response.errorMessage = error.toString()
                response.data = new TextEncoder().encode(JSON.stringify(error))
            }).finally(() => {
                send()
            })
        }
        if (action == null) send()
    }
    private isBinaryDataObject(object: any) {
        const objectType = object.__proto__.constructor.name as string
        let isbufferData = objectType == "ArrayBuffer" || objectType == "Uint8Array" || objectType == "Buffer"
        return isbufferData
    }

    /**
     Initialize local implementation meta data
     */
    private initLocalMetaData() {
        const metaData = getMetaDataObject(this.localInterface)
        function recursiveMetaData(meta: RpcMetaData) {
            meta.modules.forEach(item => {
                item.name = `${meta.name}/${item.name}`
                recursiveMetaData(item)
            })
        }
        recursiveMetaData(metaData)
        this.localRpcMetaData = metaData
    }
}

export {
    RpcFunctionRequest,
    RpcFunctionResponse,
    RpcMetaData,
    RpcBridgeHost,
    RpcBridge,
    RequestType,
    Message,
    RpcBridgeEvents,
    IRpcBridgeEvents,
    RpcMetaType
}
